<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>骨骼动画，手写方式实现</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
        /* 隐藏body窗口区域滚动条 */
      }
    </style>
    <!--引入three.js三维引擎-->
    <!-- <script src="http://www.yanhuangxueyuan.com/versions/threejsR92/build/three.js"></script> -->
    <!-- <script src="http://www.yanhuangxueyuan.com/threejs/build/three.js"></script> -->
    <!-- Three.js-master 包示例中的用法 examples/js/controls/OrbitControls.js  版本要匹配-->
    <script src="http://www.yanhuangxueyuan.com/3D/example/three.min.js"></script>
    <script src="../../util/OrbitControls.js"></script>
  </head>
  <body>
    <script type="module">
      import {
        RAFTime,
        sphereMesh,
        cylinderMesh,
        v2ToV3,
      } from "../../util/index.js";

      /**
       * 创建场景对象Scene
       */
      var scene = new THREE.Scene();

      /**
       * 辅助坐标系 参数250表示坐标系大小，可以根据场景大小去设置
       */
      var axisHelper = new THREE.AxisHelper(1000);
      scene.add(axisHelper);

      /**
       * 创建骨骼网格模型SkinnedMesh
       */
      // 创建一个圆柱几何体，高度120，顶点坐标y分量范围[-60,60]
      var geometry = new THREE.CylinderGeometry(5, 10, 120, 50, 300);
      geometry.translate(0, 60, 0); //平移后，y分量范围[0,120]
      console.log("name", geometry.vertices); //控制台查看顶点坐标
      //
      /**
       * 设置几何体对象Geometry的蒙皮索引skinIndices、权重skinWeights属性
       * 实现一个模拟腿部骨骼运动的效果
       */
      //遍历几何体顶点，为每一个顶点设置蒙皮索引、权重属性
      //根据y来分段，0~60一段、60~100一段、100~120一段
      for (var i = 0; i < geometry.vertices.length; i++) {
        var vertex = geometry.vertices[i]; //第i个顶点
        if (vertex.y <= 60) {
          // 设置每个顶点蒙皮索引属性  受根关节Bone1影响
          geometry.skinIndices.push(new THREE.Vector4(0, 0, 0, 0));
          // 设置每个顶点蒙皮权重属性
          // 影响该顶点关节Bone1对应权重是1-vertex.y/60
          geometry.skinWeights.push(
            new THREE.Vector4(1 - vertex.y / 60, 0, 0, 0)
          );
        } else if (60 < vertex.y && vertex.y <= 60 + 40) {
          // Vector4(1, 0, 0, 0)表示对应顶点受关节Bone2影响
          geometry.skinIndices.push(new THREE.Vector4(1, 0, 0, 0));
          // 影响该顶点关节Bone2对应权重是1-(vertex.y-60)/40
          geometry.skinWeights.push(
            new THREE.Vector4(1 - (vertex.y - 60) / 40, 0, 0, 0)
          );
        } else if (60 + 40 < vertex.y && vertex.y <= 60 + 40 + 20) {
          // Vector4(2, 0, 0, 0)表示对应顶点受关节Bone3影响
          geometry.skinIndices.push(new THREE.Vector4(2, 0, 0, 0));
          // 影响该顶点关节Bone3对应权重是1-(vertex.y-100)/20
          geometry.skinWeights.push(
            new THREE.Vector4(1 - (vertex.y - 100) / 20, 0, 0, 0)
          );
        }
      }
      // 材质对象
      var material = new THREE.MeshPhongMaterial({
        skinning: true, //允许蒙皮动画
        // wireframe:true,
      });
      // 创建骨骼网格模型
      var SkinnedMesh = new THREE.SkinnedMesh(geometry, material);
      SkinnedMesh.position.set(50, 120, 50); //设置网格模型位置
      SkinnedMesh.rotateX(Math.PI); //旋转网格模型
      scene.add(SkinnedMesh); //网格模型添加到场景中

      /**
       * 骨骼系统
       */
      var Bone1 = new THREE.Bone(); //关节1，用来作为根关节
      var Bone2 = new THREE.Bone(); //关节2
      var Bone3 = new THREE.Bone(); //关节3
      // 设置关节父子关系   多个骨头关节构成一个树结构
      Bone1.add(Bone2);
      Bone2.add(Bone3);
      // 设置关节之间的相对位置
      //根关节Bone1默认位置是(0,0,0)
      Bone2.position.y = 60; //Bone2相对父对象Bone1位置
      Bone3.position.y = 40; //Bone3相对父对象Bone2位置

      // 所有Bone对象插入到Skeleton中，全部设置为.bones属性的元素
      var skeleton = new THREE.Skeleton([Bone1, Bone2, Bone3]); //创建骨骼系统
      // console.log(skeleton.bones);
      // 返回所有关节的世界坐标
      // skeleton.bones.forEach(elem => {
      //   console.log(elem.getWorldPosition(new THREE.Vector3()));
      // });
      //骨骼关联网格模型
      SkinnedMesh.add(Bone1); //根骨头关节添加到网格模型
      SkinnedMesh.bind(skeleton); //网格模型绑定到骨骼系统
      console.log(SkinnedMesh);
      /**
       * 骨骼辅助显示
       */
      var skeletonHelper = new THREE.SkeletonHelper(SkinnedMesh);
      scene.add(skeletonHelper);

      // 转动关节带动骨骼网格模型出现弯曲效果  好像腿弯曲一样
      skeleton.bones[1].rotation.x = 0.5;
      skeleton.bones[2].rotation.x = 0.5;

      // 平行光
      var directionalLight = new THREE.DirectionalLight(0xffffff, 1);
      // 平行光辅助对象
      const directionalLightHelper = new THREE.DirectionalLightHelper(
        directionalLight,
        10
      );
      scene.add(directionalLightHelper);
      // 设置光源位置
      directionalLight.position.set(60, 60, 60);
      scene.add(directionalLight);
      // 设置用于计算阴影的光源对象
      directionalLight.castShadow = true;
      // 设置计算阴影的区域，最好刚好紧密包围在对象周围
      // 计算阴影的区域过大：模糊  过小：看不到或显示不完整
      directionalLight.shadow.camera.near = 0.5;
      directionalLight.shadow.camera.far = 300;
      directionalLight.shadow.camera.left = -100;
      directionalLight.shadow.camera.right = 100;
      directionalLight.shadow.camera.top = 200;
      directionalLight.shadow.camera.bottom = -200;
      // 设置mapSize属性可以使阴影更清晰，不那么模糊
      directionalLight.shadow.mapSize.set(1024, 1024);
      console.log(directionalLight.shadow.camera);

      //环境光
      var ambient = new THREE.AmbientLight(0x050505);
      scene.add(ambient);

      /**
       * 相机设置
       */
      var width = window.innerWidth; //窗口宽度
      var height = window.innerHeight; //窗口高度
      var k = width / height; //窗口宽高比
      // var s = 1; //三维场景显示范围控制系数，系数越大，显示的范围越大
      // //创建相机对象
      // var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 100);
      // ---------------------------------------目8.9 环境贴图，使用PerspectiveCamera比OrthographicCamera更真正，人眼视角
      var camera = new THREE.PerspectiveCamera(60, k, 1, 2000);
      camera.position.set(292, 109, 268); //设置相机位置
      camera.lookAt(scene.position); //设置相机方向(指向的场景对象)

      /**
       * 创建渲染器对象
       */
      var renderer = new THREE.WebGLRenderer();
      renderer.setSize(width, height); //设置渲染区域尺寸
      renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色
      // 设置渲染器，允许光源阴影渲染
      renderer.shadowMap.enabled = true;
      document.body.appendChild(renderer.domElement); //body元素中插入canvas对象

      // // 监听鼠标、键盘事件，实现 旋转/缩放/平移
      // function render() {
      //   renderer.render(scene, camera); //执行渲染操作
      // }

      var n = 0;
      var T = 50;
      var step = 0.01;

      // 递归函数,每1/60秒左右,调用一次自己
      const render = RAFTime((t) => {
        renderer.render(scene, camera); //执行渲染操作

        n += 1;
        if (n < T) {
          // 改变骨关节角度
          skeleton.bones[0].rotation.x = skeleton.bones[0].rotation.x - step;
          skeleton.bones[1].rotation.x = skeleton.bones[1].rotation.x + step;
          skeleton.bones[2].rotation.x =
            skeleton.bones[2].rotation.x + 2 * step;
        }
        if (n < 2 * T && n > T) {
          skeleton.bones[0].rotation.x = skeleton.bones[0].rotation.x + step;
          skeleton.bones[1].rotation.x = skeleton.bones[1].rotation.x - step;
          skeleton.bones[2].rotation.x =
            skeleton.bones[2].rotation.x - 2 * step;
        }
        if (n === 2 * T) {
          n = 0;
        }
      });

      // 启动渲染---requestAnimationFrame周期性的渲染
      render();

      // 创建，轨迹控件---实现旋转/平移/缩放
      var controls = new THREE.OrbitControls(camera, renderer.domElement);
      // // 监听鼠标、键盘事件
      // controls.addEventListener("change", (e) => {
      //   console.log("e---", e);
      // });

      // 窗口大小变化后，设置画布 => 设置相机参数 => 更新投影矩阵
      window.onresize = function () {
        // 重置渲染器输出画布canvas尺寸
        renderer.setSize(window.innerWidth, window.innerHeight);
        // 全屏情况下：设置观察范围长宽比aspect为窗口宽高比
        camera.aspect = window.innerWidth / window.innerHeight;
        // 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
        // 但是不会每渲染一帧，就通过相机的属性计算投影矩阵(节约计算资源)
        // 如果相机的一些属性发生了变化，需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
        camera.updateProjectionMatrix();
      };
    </script>
  </body>
</html>
